/*
   (c) 2009 Creative Development LLC. All rights reserved.
                 http://www.ecwid.com/
*/


package ru.cdev.management;

import java.io.IOException;
import java.io.Writer;
import java.lang.reflect.Array;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Map;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;

import javax.management.AttributeNotFoundException;
import javax.management.InstanceNotFoundException;
import javax.management.IntrospectionException;
import javax.management.MBeanAttributeInfo;
import javax.management.MBeanException;
import javax.management.MBeanInfo;
import javax.management.MBeanOperationInfo;
import javax.management.MBeanParameterInfo;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectInstance;
import javax.management.ObjectName;
import javax.management.ReflectionException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.StringEscapeUtils;
import org.springframework.jmx.support.JmxUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import ru.cdev.ws.AuthoriserHelper;
import ru.cdev.ws.UserPasswordCredentials;

/**
 * Web (REST & human) interface for jmx.
 * 
 * @author Dmitry Negoda <Dmitry.Negoda@gmail.com>
 */
@Controller
public class WebAdapter {

	private static final String header = "<html><head><style>li{display:block;margin-top:4px;} .descr{font-style:italic;display:block} .value:before{content:'='} input{width:250px}</style></head><body>";
	private final DateFormat sdf = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss z");

	private String user;
	private String password;
	private Logger log = Logger.getLogger(WebAdapter.class.getName());

	/**
	 * human-readable MBean list
	 */
	@RequestMapping("/jmx/")
	public void root(HttpServletRequest req, HttpServletResponse resp) throws IOException {
		if (!auth(req, resp)) return;
		MBeanServer mbs = JmxUtils.locateMBeanServer();
		TreeSet<String> names = new TreeSet<String>();
		for (ObjectInstance oi: mbs.queryMBeans(null, null)) {
			names.add(oi.getObjectName().getCanonicalName());
		}
		ServletOutputStream outputStream = resp.getOutputStream();
		outputStream.println(header +"<ul>");
		for (String name: names) {
			outputStream.println("<li><a href='"+URLEncoder.encode(name, "UTF-8")+"/info'>"+name+"</a></li>");
		}
		outputStream.println("</ul>");
	}
	
	/**
	 * human-readable information about an MBean 
	 */
	@RequestMapping("/jmx/*/info")
	public void info(HttpServletRequest req, HttpServletResponse resp) throws IOException, IntrospectionException, InstanceNotFoundException, MalformedObjectNameException, ReflectionException, NullPointerException, AttributeNotFoundException, MBeanException {
		resp.setCharacterEncoding("UTF-8");
		if (!auth(req, resp)) {
			return;
		}

		String uri = req.getRequestURI();
		String components[] = uri.split("/");
		String name = components[components.length - 2];
		ObjectName oname = new ObjectName(URLDecoder.decode(name, "UTF-8"));

		MBeanServer mbs = JmxUtils.locateMBeanServer();
		MBeanInfo mBeanInfo = mbs.getMBeanInfo(oname);

		Writer outputStream = resp.getWriter();
		outputStream.write(header + "<ul class='attrs'>\n");
		for (MBeanAttributeInfo attributeInfo : mBeanInfo.getAttributes()) {
			outputStream.write("<li><span class='descr'>" + attributeInfo.getDescription() + "</span>\n");
			outputStream.write("<span class='name'>" + getType(attributeInfo.getType()) + " <b>" + attributeInfo.getName() + "</b></span>\n");
			Object value = mbs.getAttribute(oname, attributeInfo.getName());
			outputStream.write("<span class='value'>" + toString(getType(attributeInfo.getType()), value) + "</span></li>\n");
			outputStream.write("<br/>\n");
		}
		outputStream.write("</ul>\n");

		outputStream.write("<ul class='ops'>");
		for (MBeanOperationInfo opInfo : mBeanInfo.getOperations()) {
			outputStream.write("<li><span class='descr'>" + opInfo.getDescription() + "</span>\n");
			StringBuilder params = new StringBuilder();
			for (MBeanParameterInfo paramInfo : opInfo.getSignature()) {
				if (params.length() > 0) params.append(',');
				params.append(getType(paramInfo.getType())).append(" <b>").append(paramInfo.getName()).append("</b>/* ").append(paramInfo.getDescription()).append(" */");
			}
			outputStream.write("<span class='name'>" + getType(opInfo.getReturnType()) + " <a href='" + opInfo.getName() + "/call'>" + opInfo.getName() + "</a>(" + params + ")</span></li>\n");
			outputStream.write("<br/>\n");
		}
		outputStream.write("</ul>\n");
	}
	
	/**
	 * A GET request which returns an MBean attribute value 
	 */
	@RequestMapping(value = "/jmx/*/*/get", method = {RequestMethod.GET})
	public void get(HttpServletRequest req, HttpServletResponse resp) throws IOException, IntrospectionException, InstanceNotFoundException, MalformedObjectNameException, ReflectionException, NullPointerException, AttributeNotFoundException, MBeanException {
		if (!auth(req, resp)) return;
		resp.setCharacterEncoding("UTF-8");
		resp.setContentType("text/plain");
		String uri = req.getRequestURI();
		uri = uri.substring(0, uri.lastIndexOf('/'));
		String attrName = URLDecoder.decode(uri.substring(uri.lastIndexOf('/')+1), "UTF-8");
		uri = uri.substring(0, uri.lastIndexOf('/'));
		ObjectName oname = new ObjectName(URLDecoder.decode(uri.substring(uri.lastIndexOf('/')+1), "UTF-8"));
		uri.lastIndexOf('/');
		MBeanServer mbs = JmxUtils.locateMBeanServer();
		MBeanInfo mBeanInfo = mbs.getMBeanInfo(oname);
		Writer outputStream = resp.getWriter();
		
		try {
			for (MBeanAttributeInfo attr: mBeanInfo.getAttributes()) {
				if (!attr.getName().equals(attrName)) continue;
				Object result = mbs.getAttribute(oname, attrName);
				String stringResult = toString(getType(attr.getType()), result);
				outputStream.write(stringResult);
			}
		} catch (Exception ex) {
			resp.sendError(500, StringEscapeUtils.escapeXml(ex.getMessage()));
		}
	}

	/**
	 * A POST request which calls an MBean method 
	 */
	@RequestMapping(value = "/jmx/*/*", method = {RequestMethod.POST})
	public void call(HttpServletRequest req, HttpServletResponse resp) throws IOException, IntrospectionException, InstanceNotFoundException, MalformedObjectNameException, ReflectionException, NullPointerException, AttributeNotFoundException, MBeanException {
		if (!auth(req, resp)) return;
		resp.setCharacterEncoding("UTF-8");
		resp.setContentType("text/plain");
		String uri = req.getRequestURI();
		String methodName = URLDecoder.decode(uri.substring(uri.lastIndexOf('/')+1), "UTF-8");
		uri = uri.substring(0, uri.lastIndexOf('/'));
		ObjectName oname = new ObjectName(URLDecoder.decode(uri.substring(uri.lastIndexOf('/')+1), "UTF-8"));
		uri.lastIndexOf('/');
		MBeanServer mbs = JmxUtils.locateMBeanServer();
		MBeanInfo mBeanInfo = mbs.getMBeanInfo(oname);
		ServletOutputStream outputStream = resp.getOutputStream();
		
		try {
			for (MBeanOperationInfo op: mBeanInfo.getOperations()) {
				if (!op.getName().equals(methodName)) continue;
				Object[] params = new Object[op.getSignature().length];
				String[] sig = new String[op.getSignature().length];
				for (int i=0; i<op.getSignature().length; i++) {
					MBeanParameterInfo parameterInfo = op.getSignature()[i];
					log.fine(parameterInfo.getName()+"="+req.getParameter(parameterInfo.getName()));
					params[i] = toObject(parameterInfo.getType(), req.getParameter(parameterInfo.getName()));
					sig[i] = parameterInfo.getType();
				}
				Object result = mbs.invoke(oname, methodName, params, sig);
				String stringResult = toString(getType(op.getReturnType()), result);
				outputStream.print(stringResult);
			}
		} catch (Exception ex) {
			log.log(Level.WARNING, "cannot call JMX method "+methodName, ex);
			resp.sendError(500, StringEscapeUtils.escapeXml(ex.getMessage()));
		}
	}
	
	/**
	 * human-readable html form for calling a method
	 */
	@RequestMapping(value = "/jmx/*/*/call", method = {RequestMethod.GET})
	public void callForm(HttpServletRequest req, HttpServletResponse resp) throws IOException, IntrospectionException, InstanceNotFoundException, MalformedObjectNameException, ReflectionException, NullPointerException, AttributeNotFoundException, MBeanException {
		if (!auth(req, resp)) return;
		String uri = req.getRequestURI();
		String components[] = uri.split("/");
		String methodName = URLDecoder.decode(components[components.length-2], "UTF-8");
		ObjectName oname = new ObjectName(URLDecoder.decode(components[components.length-3], "UTF-8"));
		MBeanServer mbs = JmxUtils.locateMBeanServer();
		MBeanInfo mBeanInfo = mbs.getMBeanInfo(oname);
		ServletOutputStream outputStream = resp.getOutputStream();
		outputStream.println(header);
		outputStream.println("<h1>"+oname.getCanonicalName()+"."+methodName+"</h1><form action='../"+components[components.length-2]+"' method='POST'><table border='0'>");
		
		for (MBeanOperationInfo op: mBeanInfo.getOperations()) {
			if (!op.getName().equals(methodName)) continue;
			Object[] params = new Object[op.getSignature().length];
			String[] sig = new String[op.getSignature().length];
			for (int i=0; i<op.getSignature().length; i++) {
				MBeanParameterInfo parameterInfo = op.getSignature()[i];
				outputStream.println("<tr><td>"+parameterInfo.getType()+" "+parameterInfo.getName()+"</td>");
				outputStream.println("<td><input name='"+parameterInfo.getName()+"'></td></tr>");
			}
			outputStream.println("</table><input type='submit'></form>");
			return;
		}
		
	}
	
	private String getType(String type) {
		type = type.replaceAll(Pattern.quote("java.lang."), "");
		if (type.startsWith("[L") && type.endsWith(";")) {
			type = type.substring(2, type.length() - 1) + "[]";
		}

		// TODO: could add support for arrays of primitive types here

		return type;
	}

	private String toString(String type, Object result) {
		if (result == null) {
			return "null";
		}

		if ("java.util.Date".equals(type)) {
			return sdf.format((java.util.Date)result);
		}

		if (result.getClass().isArray()) {
			return arrayToString(result);
		}
		
		if (result instanceof Number) {
			DecimalFormatSymbols s = new DecimalFormatSymbols() {
				public char getDecimalSeparator() {
					return '.';
				}
			};
			DecimalFormat df = new DecimalFormat("##0.##########", s);
			return df.format(((Number)result).doubleValue());
		} else if (result instanceof Map) {
			// display maps as json
			StringBuilder sb = new StringBuilder("{");
			boolean first = true;
			Escaper esc = new Escaper(false);
			for (Map.Entry<?,?> entry: ((Map<?,?>)result).entrySet()) {
				if (first) first = false;
				else sb.append(",\n");
				sb.append("\"").append(esc.escapeJsonString(entry.getKey().toString())).append("\"");
				sb.append(':');
				if (entry.getValue() == null) sb.append("null");
				else {
					String valString = toString(entry.getValue().getClass().getName(), entry.getValue());
					sb.append("\"").append(esc.escapeJsonString(valString)).append("\"");
				}
			}
			sb.append('}');
			return sb.toString();
		}

		return result.toString();
	}

	private String arrayToString(Object array) {
		if (!array.getClass().isArray()) {
			throw new IllegalArgumentException("argument is not array");
		}

		StringBuilder sb = new StringBuilder();
		Class<?> arrayClass = array.getClass();
		int arrayLength = Array.getLength(array);
		if (arrayClass.equals(int[].class)) {
			int[] arr = (int[]) array;
			for (int i = 0; i < arrayLength; i++) {
				sb.append(arr[i]);
				if (i < (arrayLength - 1)) {
					sb.append("<br/>");
				}
			}
		} else if (arrayClass.equals(long[].class)) {
			long[] arr = (long[]) array;
			for (int i = 0; i < arrayLength; i++) {
				sb.append(arr[i]);
				if (i < (arrayLength - 1)) {
					sb.append("<br/>");
				}
			}
		} else if (arrayClass.equals(short[].class)) {
			short[] arr = (short[]) array;
			for (int i = 0; i < arrayLength; i++) {
				sb.append(arr[i]);
				if (i < (arrayLength - 1)) {
					sb.append("<br/>");
				}
			}
		} else if (arrayClass.equals(byte[].class)) {
			byte[] arr = (byte[]) array;
			for (int i = 0; i < arrayLength; i++) {
				sb.append(arr[i]);
				if (i < (arrayLength - 1)) {
					sb.append("<br/>");
				}
			}
		} else if (arrayClass.equals(double[].class)) {
			double[] arr = (double[]) array;
			for (int i = 0; i < arrayLength; i++) {
				sb.append(arr[i]);
				if (i < (arrayLength - 1)) {
					sb.append("<br/>");
				}
			}
		} else if (arrayClass.equals(float[].class)) {
			float[] arr = (float[]) array;
			for (int i = 0; i < arrayLength; i++) {
				sb.append(arr[i]);
				if (i < (arrayLength - 1)) {
					sb.append("<br/>");
				}
			}
		} else if (arrayClass.equals(boolean[].class)) {
			boolean[] arr = (boolean[]) array;
			for (int i = 0; i < arrayLength; i++) {
				sb.append(arr[i]);
				if (i < (arrayLength - 1)) {
					sb.append("<br/>");
				}
			}
		} else if (arrayClass.equals(char[].class)) {
			char[] arr = (char[]) array;
			for (int i = 0; i < arrayLength; i++) {
				sb.append(arr[i]);
				if (i < (arrayLength - 1)) {
					sb.append("<br/>");
				}
			}
		} else {
			Object[] arr = (Object[]) array;
			for (int i = 0; i < arrayLength; i++) {
				Object value = arr[i];
				String type = value == null ? "null" : value.getClass().getName();
				sb.append(toString(type, value));
				if (i < (arrayLength - 1)) {
					sb.append("<br/>");
				}
			}
		}

		if (sb.length() == 0) {
			sb.append("[]");
		} else {
			sb.insert(0, "[<br/><div style='margin-left:30px'>");
			sb.append("</div>]");
		}
		return sb.toString();
	}
	
	/**
	 * Convert a string entered by a user into a Java object
	 */
	private Object toObject(String type, String parameter) throws ParseException {
		if (parameter.equals("null")) return null;
		if (type.equals("int")) return Integer.parseInt(parameter);
		if (type.equals("long")) return Long.parseLong(parameter);
		if (type.equals("double")) return Double.parseDouble(parameter);
		if (type.equals("boolean")) return Boolean.parseBoolean(parameter);
		if (type.equals("java.lang.String")) return parameter;
		if (type.equals("java.util.Date")) return parameter.trim().isEmpty() ? null : sdf.parse(parameter);
		if (type.startsWith("[")) {
			// arrays
			String[] elements = parameter.split(" *, *");
			if (type.equals("[I")) {
				Object array = Array.newInstance(int.class, elements.length);
				for (int i=0; i<elements.length; i++) {
					Array.set(array, i, Integer.parseInt(elements[i]));
				}
				return array;
			}
		}
		Class<?> claz;
		try {
			claz = Class.forName(type);
			if (claz.isEnum()) {
				StringBuilder possible = new StringBuilder();
				for (Object constant: claz.getEnumConstants()) {
					if (constant.toString().equals(parameter)) return constant;
					possible.append(' ').append(constant.toString());
				}
				throw new ParseException("Unknown constant "+parameter+"; possible values are: "+possible,0);
			}
			throw new ParseException("Don't know how to parse values of the type "+type, 0);
		} catch (ClassNotFoundException e) {
			throw new ParseException("Cannot find class "+type, 0);
		}
	}

	/**
	 * Check HTTP login/password and issues 401 if error 
	 * @return True, if the login/password is correct
	 */
	private boolean auth(HttpServletRequest req, HttpServletResponse resp) throws IOException {
		UserPasswordCredentials cred = AuthoriserHelper.parseHttpRequest(req);
		if (cred == null || !cred.getUser().equals(user) || !cred.getPassword().equals(password)) {
			resp.setHeader("WWW-Authenticate", "Basic realm=\"my.ecwid.com Superuser\"");
			resp.sendError(401);
			return false;
		}
		return true;
	}

	public String getUser() {
		return user;
	}

	public void setUser(String user) {
		this.user = user;
	}

	public String getPassword() {
		return password;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	
}
